---
title: 开发web应用
description: 使用Spring MVC 展现模型数据和处理表单输入
---
使用Spring MVC 展现模型数据和处理表单输入
## 展现信息
Taco Cloud 网站是一个可以在在线订购 taco 的地方。用户可以通过丰富的配料设计自己的 taco。

因此，需要有一个页面为展现可以选择的配料。可选的配料随时可能发送变化，所以不能硬编码到 HTML 页面中。应该从数据库中获取可用的配料并将其传递到页面，进而展现给用户。

在 Spring MVC 应用中，**获取和处理数据是控制器的任务，而将数据渲染到 HTML 并在浏览器展现是视图的任务。**

为了显示 taco 的创建页面，我们需要构建如下的组件：

* taco 配料类
* 用于获取配料信息并将其传递至视图的 SpringMVC 控制器类。
* 用来在用户的浏览器中渲染配料列表的视图模板。

这些组件的关系如下：



![c6d32700813ab77d1g0136f4](https://img.wkq.pub/hexo/c6d32700813ab77d1g0136f4.png)

### 构建领域类



![CB_3300044333_15](https://img.wkq.pub/hexo/15.png)



```java title='定义 taco 配料'
package pub.wkq.springboot.pojo;

import lombok.Data;

/**
 * <h2>定义 taco 配料类</h2>
 *
 * @author 王开琦
 */
@Data
public class Ingredient {
    private final String id;
    private final String name;
    private final Type type;

    public enum Type {
        //包裹
        WRAP,
        //蛋白质
        PROTEIN,
        //蔬菜
        VEGGIES,
        //奶酪
        CHEESE,
        //调味汁
        SAUCE;
    }
}
```

```java title='定义 taco 类'
package pub.wkq.springboot.pojo;

import lombok.Data;

import java.util.List;

/**
 * <h1>定义 Taco 类</h1>
 *
 * @author 王开琦
 */
@Data
public class Taco {
    private String name;
    private List<Ingredient> ingredients;
}
```

```java title='定义 Taco 订单类'
package pub.wkq.springboot.pojo;

import lombok.Data;

import java.util.ArrayList;
import java.util.List;

/**
 * <h1>Taco 订单类</h1>
 *
 * @author 王开琦
 */
@Data
public class TacoOrder {
    /**
     * 5 个投递相关的信息
     */
    private String deliverName;
    private String deliverStreet;
    private String deliverCity;
    private String deliverState;
    private String deliveryZip;
    /**
     * 3 个支付相关的信息
     */
    private String ccNumber;
    private String ccExpiration;
    private String ccCVV;
    /**
     * Taco 对象列表
     */
    public List<Taco> tacos = new ArrayList<>();

    public void addTaco(Taco taco) {
        tacos.add(taco);
    }
}
```


### 构建控制器类
在 Spring MVC 框架中，控制器是重要的参与者。它们的主要职责是处理 HTTP 请求，要么将数据传递给视图以便渲染 HTML（浏览器展现），要么直接将数据写入响应体（RESTful）。

对于 Taco Cloud 应用来说，需要一个简单的控制器，它要完成如下的功能：
* 处理路径为 `/design` 的 HTTP GET 请求；
* 构建配料的列表；
* 处理请求，并将配料数据传递给要渲染为 HTML 的视图模板，然后发送给发起请求的浏览器。

```java title='设计 Taco 的控制器'
package pub.wkq.springboot.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
import pub.wkq.springboot.pojo.Ingredient;
import pub.wkq.springboot.pojo.Taco;
import pub.wkq.springboot.pojo.TacoOrder;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import static pub.wkq.springboot.pojo.Ingredient.Type;

/**
 * 设计 Taco 的控制器
 * 若希望在多个请求之间共享数据，则可以在控制器类上标注一个 @SessionAttributes("tacoOrder")
 * 将 model 中属性名为 tacoOrder 的数据 copy 一份进了 session 域中。
 *
 * @author 王开琦
 */
@Controller
@Slf4j
@RequestMapping("/design")
@SessionAttributes("tacoOrder")
public class DesignTacoController {
    /**
     * 被 @ModelAttribute 注解的方法会在 Controller 每个方法执行之前都执行，因此对于一个 Controller 中包含多个 URL 的时候,要谨慎使用。
     *
     * @param model
     */
    @ModelAttribute
    public void addIngredientsToModel(Model model) {
        List<Ingredient> ingredients = Arrays.asList(
                new Ingredient("FLTO", "Flour Tortilla", Type.WRAP),
                new Ingredient("COTO", "Corn Tortilla", Type.WRAP),
                new Ingredient("GRBF", "Ground Beef", Type.PROTEIN),
                new Ingredient("CARN", "Carnitas", Type.PROTEIN),
                new Ingredient("TMTO", "Diced Tomatoes", Type.VEGGIES),
                new Ingredient("LETC", "Lettuce", Type.VEGGIES),
                new Ingredient("CHED", "Cheddar", Type.CHEESE),
                new Ingredient("JACK", "Monterrey Jack", Type.CHEESE),
                new Ingredient("SLSA", "Salsa", Type.SAUCE),
                new Ingredient("SRCR", "Sour Cream", Type.SAUCE)
        );
        Type[] types = Type.values();
        log.info("types [{}]", types);
        for (Type type : types) {
            model.addAttribute(type.toString().toLowerCase(), filterByType(ingredients, type));
        }
    }

    private Iterable<Ingredient> filterByType(List<Ingredient> ingredients, Type type) {
        return ingredients.stream().filter(x -> x.getType().equals(type)).collect(Collectors.toList());
    }

    @ModelAttribute(name = "tacoOrder")
    public TacoOrder order() {
        return new TacoOrder();
    }

    @ModelAttribute(name = "taco")
    public Taco taco() {
        return new Taco();
    }

    @GetMapping
    public String showDesignForm() {
        return "design";
    }
}
```
DesignTacoController 用到的下一个注解是 `@Controller`。这个注解会将这个类识别为控制器，并将其作为组件扫描的候选者，所以 Spring 会发现它并自动创建一个 DesignTacoController 实例，并将该实例作为 Spring 应用上下文中的 bean。
`@RequestMapping`用在类级别的时候，指定该控制器所处理请求的范围。

`@SessionAttributes("tacoOrder")`注解表示会将这个 model 对象保存在 session 中。使其能够跨越多个请求。

`@GetMapping` 对类级别的 `@RequestMapping` 进行了细化。`@GetMapping`结合类级别的  `@RequestMapping`,指明当接收到对 ”/design“ 的 HTTP GET 请求时，Spring MVC 将会调用 showDesignForm() 来处理请求。它会返回一个值为 ”design“ 的 String，

这是视图的逻辑名称，用来向浏览器渲染模型

Spring MVC 请求映射注解。

| 注解            | 描述                |
| --------------- | ------------------- |
| @RequestMapping | 通用的请求处理      |
| @GetMapping     | 处理HTTP GET请求    |
| @PostMapping    | 处理HTTP POST请求   |
| @PutMapping     | 处理HTTP PUT请求    |
| @DeleteMapping  | 处理HTTP DELETE请求 |
| @PatchMapping   | 处理HTTP PATCH请求  |

带有`@ModalAttribute` 注解的方法也会在请求处理的时候被调用。addIngredientsToModel 方法生成的配料类型的列表会以属性的形式添加到 Model 对象上，并传递给 showDesignForm 方法。Model 对象负责在控制器和展现数据的视图之间传递数据。

:::tip

放到 Model 属性中的数据会复制到 Servlet Request 的属性中，这样视图就能找到它们，并使它们在用户的浏览器中渲染页面。

:::

order  和 taco 方法会创建 TacoOrder 和 Taco 对象放到模型中。

如果现在应用并在浏览器上访问 ”/design“ 路径， 会在请求传递给视图之前，将配料和一个空的 Taco 放置到模型中。


### 设计视图

像 Thymeleaf 这样的视图库在设计时是与特定的 Web 框架解耦的。它们无法感知 Spring 的模型抽象，所以，在 Spring 将请求转发到视图之前，它会把模型数据复制到 request 属性中，Thymeleaf 和其它视图模板就可以访问到它们了。

Thymeleaf 模板就是增加一些额外属性的 HTML，这些属性能够指导模板如何渲染 request 数据。

```html title='如果有个请求属性的key为 ”message“，我们想要将其渲染到 HTML <p> 标签中，那么在 Thymeleaf 模板中，可以这样写：'
<p th:text="${message}">placeholder message</p>
```

模板渲染成 HTML 时，`<p>`元素体会被替换为 Servlet request 中 key 为 message 的属性值。”th:text“ 是 Thymeleaf 命名空间中的属性，它会执行这个替换过程。`${}`操作符会告诉它要使用某个 request 属性的值。

Thymeleaf 还提供了另外一个属性：th:each， 它会迭代一个元素集合，为集合中的每个条目渲染 HTML。

```html title='渲染wrap配料的列表'
<!DOCTYPE html>
<html xmlns = "http://www.w3.org/1999/xhtml"
      xmlns:th = "http://www.thymeleaf.org">
<head>
    <title>Taco Cloud</title>
    <link rel = "stylesheet" th:href = "@{/styles.css}" />
</head>
<body>
<h1>设计你的taco!</h1>
<img th:src = "@{/images/TacoCloud.png}"/>

<form method = "POST" th:object = "${taco}">
    <div class = "grid">
        <div class = "ingredient-group" id = "wraps">
            <h3>Designate your wrap:</h3>
            <div th:each = "ingredient : ${wrap}">
                <input th:field = "*{ingredients}" type = "checkbox"
                       th:value = "${ingredient.id}"/>
                <span th:text = "${ingredient.name}">INGREDIENT</span><br/>
            </div>
        </div>

        <div class = "ingredient-group" id = "proteins">
            <h3>Pick your protein:</h3>
            <div th:each = "ingredient : ${protein}">
                <input th:field = "*{ingredients}" type = "checkbox"
                       th:value = "${ingredient.id}"/>
                <span th:text = "${ingredient.name}">INGREDIENT</span><br/>
            </div>
        </div>

        <div class = "ingredient-group" id = "cheeses">
            <h3>Choose your cheese:</h3>
            <div th:each = "ingredient : ${cheese}">
                <input th:field = "*{ingredients}" type = "checkbox"
                       th:value = "${ingredient.id}"/>
                <span th:text = "${ingredient.name}">INGREDIENT</span><br/>
            </div>
        </div>

        <div class = "ingredient-group" id = "veggies">
            <h3>Determine your veggies:</h3>
            <div th:each = "ingredient : ${veggies}">
                <input th:field = "*{ingredients}" type = "checkbox"
                       th:value = "${ingredient.id}"/>
                <span th:text = "${ingredient.name}">INGREDIENT</span><br/>
            </div>
        </div>

        <div class = "ingredient-group" id = "sauces">
            <h3>Select your sauce:</h3>
            <div th:each = "ingredient : ${sauce}">
                <input th:field = "*{ingredients}" type = "checkbox"
                       th:value = "${ingredient.id}"/>
                <span th:text = "${ingredient.name}">INGREDIENT</span><br/>
            </div>
        </div>
    </div>

    <div>
        <h3>Name your taco creation:</h3>
        <input type = "text" th:field = "*{name}"/>
        <br/>

        <button>Submit Your Taco</button>
    </div>
</form>
</body>
</html>
```
```css title='样式文件'
div.ingredient-group:nth-child(odd) {
	float: left;
	padding-right: 20px;
}

div.ingredient-group:nth-child(even) {
    float: left;
	padding-right: 0;
}

div.ingredient-group {
	width: 50%;
}

.grid:after {
  content: "";
  display: table;
  clear: both;
}

*, *:after, *:before {
  -webkit-box-sizing: border-box;
  -moz-box-sizing: border-box;
  box-sizing: border-box;
}

span.validationError {
	color: red;
}
```
## 校验表单输入
Spring 支持 JavaBean 校验 API（JavaBean Validation API，也称为 JSR-303），使我们能够更容易的声明校验规则，而不必在应用程序代码中显式编写声明逻辑。
使用步骤：
1. 在构建文件中添加 Spring Validation Starter
2. 在被要校验的类上声明校验规则。
3. 在需要校验的控制器方法中声明要进行校验。

Validation API 提供了一些注解，可以添加到领域对象的属性上，以便声明校验规则。Hibernate 的 Validation API 实现又添加了一些校验注解。通过将 Spring Validation starter 添加到构建文件中，我们就能将这两者引入项目中。
```xml
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
```
### 声明校验规则
```java title='为 Taco 领域类添加校验'
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;

import java.util.List;

/**
 * <h1>定义 Taco 类</h1>
 *
 * @author 王开琦
 */
@Data
public class Taco {
    @NotNull
    @Size(min = 5, message = "名称最小为5个字符长！")
    private String name;
    @NotNull
    @Size(min = 1,message = "你必须选择最少一种配料！")
    private List<Ingredient> ingredients;
}
```
在对 taco 订单进行校验时，必须要给 TacoOrder 类添加注解。对于地址相关的属性，我们只想确保用户没有提交空白字段，可以使用 `@NotBlack` 注解.

```java title='校验订单的字段'
package pub.wkq.springboot.pojo;

import java.util.List;
import java.util.ArrayList;

import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import org.hibernate.validator.constraints.CreditCardNumber;

@Data
public class TacoOrder {
    @NotBlank(message = "deliveryName 是必填字段")
    private String deliveryName;
    @NotBlank(message = "deliveryStreet 是必填字段")
    private String deliveryStreet;
    @NotBlank(message = "deliveryCity 是必填字段")
    private String deliveryCity;
    @NotBlank(message = "deliveryState 是必填字段")
    private String deliveryState;
    @NotBlank(message = "deliveryZip 是必填字段")
    private String deliveryZip;
    @CreditCardNumber(message = "不是一个合法的信用卡号！")
    private String ccNumber;
    @Pattern(regexp = "^(0[1-9]|1[0-2])([\\/])([2-9][0-9])$", message = "必须匹配 MM/YY")
    private String ccExpiration;
    @Digits(integer = 3, fraction = 0, message = "Invalid CVV")
    private String ccCVV;

    private List<Taco> tacos = new ArrayList<>();

    public void addTaco(Taco taco) {
        this.tacos.add(taco);
    }
}
```
### 执行校验
要校验提交的 Taco，需要在 DesignTacoController 中的 processTaco() 方法的 Taco 参数添加一个 JavaBean Validation API 的 @Valid 注解。
```java
 @PostMapping
    public String processTaco(@Valid Taco taco, Errors errors, @ModelAttribute TacoOrder tacoOrder) {
        if (errors.hasErrors()) {
            return "design";
        }
        tacoOrder.addTaco(taco);
        log.info("Processing taco: {}", taco);
        //重定向视图，重定向到订单表单页面。
        return "redirect:/orders/current";
    }
```
## 使用视图控制器
```java title='ebMVCConfigurer 定义了多个方法来配置 Spring MVC，尽管只是一个接口，却提供了所有方法的默认实现，只需要我们覆盖所需方法的默认实现。'
/**
 * W
 *
 * addViewControllers() 方法会
 * @author 王开琦
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {
    /**
     * addViewControllers() 方法会接受一个 ViewControllerRegistry 对象，我们可以使用它注册一个或多个视图控制器。在这里，使用 registry  的 addViewController() 方法，将 ”/“ 传递进去
     * ，视图控制器会针对 该路径执行 GET 请求。这个方法会返回 ViewControllerRegistration 对象，然后就基于该对象调用 setViewName() 方法，用它指明当请求 ”/“ 的时候要转发到 home 视图上
     *
     * 现在已经用几行代码就替换了 HomeController ，现在可以删除 HomeController ;
     * @param registry
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("home");
    }
}

```
addViewControllers() 方法会接受一个 ViewControllerRegistry 对象，我们可以使用它注册一个或多个视图控制器。在这里，使用 registry  的 addViewController() 方法，将 ”/“ 传递进去
 ，视图控制器会针对 该路径执行 GET 请求。这个方法会返回 ViewControllerRegistration 对象，然后就基于该对象调用 setViewName() 方法，用它指明当请求 ”/“ 的时候要转发到 home 视图上

 现在已经用几行代码就替换了 HomeController ，现在可以删除 HomeController ;

## 总结
1. Spring 提供了一个强大的 Web 框架，名为 Spring MVC ，它能够用来为 Spring应用开发 Web 前端。
2. Spring MVC 是基于注解的，通过像 @RequestMapping 、@GetMapping 和 @PostMapping  这样的注解来启用请求处理方法的声明。
3. 大多数的请求处理方法会返回一个视图的逻辑名称，比如 Thymeleaf 模板，请求会转发到这样的视图上（同时会带有任意的模型数据）。
4. Spring MVC 支持校验，这是通过 JavaBean Validation API 和 Validation API 的实现（如 Hibernate Validator ）完成的。
5. 我们可以在 WebMvcConfigurer 类中通过 addViewController 方法注册视图控制器，以便处理没有模型数据和业务逻辑的 HTTP
 GET 请求。

